/*
 * Copyright (C) 2014 Nathan Templon
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package data;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Scanner;
import java.util.TreeMap;
import log.Logger;
import plugin.LibraryLoader;
import settings.Settings;

/**
 * A class for reading in and storing data from config files.
 * @author Nathan Templon
 */
public class ObjectDB {
    
    // -------------------------------------------------------------------------------------------------
    // ---------------------------  Constructors  ------------------------------------------------------
    // -------------------------------------------------------------------------------------------------
    
    /**
     * The default constructor.
     */
    public ObjectDB() {
        pathMap = new TreeMap<>();
        nodes = new ArrayList<>();
        dataMap = new TreeMap<>();
        loaders = new ArrayList<>();
        badObjects = new ArrayList<>();
        logger = Logger.getDefaultLogger();
    }
    
    
    
    
    
    // -------------------------------------------------------------------------------------------------
    // ---------------------------  Database Query Methods  --------------------------------------------
    // -------------------------------------------------------------------------------------------------
    
    
    /**
     * A method for checking if the database's PATH mappings contain the provided key.
     * @param key The key to be checked for presence in the database's PATH mappings.
     * @return Returns true if the path is present in the mappings, and false if it is not.
     */
    public boolean containsPathKey(String key) {
        return pathMap.containsKey(key);
    }
    
    
    /**
     * A method for checking if the database contains an Object with the specified names.
     * @param key the name to be queried against the database's Object names
     * @return Returns true if the database contains an Object with the name specified, false if it does not.
     */
    public boolean containsObjectKey(String key) {
        return dataMap.containsKey(key);
    }
    
    
    /**
     * A method for getting the path mapped to the specified key via PATH nodes.
     * @param key The key to receive the mapping for.
     * @return Returns the appropriate mapping if the key is present in the PATH mappings, null if it does not.
     */
    public String getPath(String key) {
        if (pathMap.containsKey(key)) {
            return pathMap.get(key);
        }
        else {
            return null;
        }
    }
    
    
    /**
     * A method for getting the Object in the database with the specified name.
     * @param key the name of the Object to be retrieved
     * @return Returns the Object with the provided name if it is present in the database, and null if there is no such Object.
     */
    public Object getObject(String key) {
        if (!dataMap.containsKey(key)) {
            return null;
        }
        return dataMap.get(key);
    }
    
    
    /**
     * A method that returns classes from libraries 
     * @param cName the fully qualified name of the class to be obtained from the libraries
     * @return Returns a Class object of the class if it exists in a library, and null if it does not exist in any library.
     * @throws java.lang.ClassNotFoundException if the class requested is not in the libraries
     */
    public Class<?> getLibraryClass( String cName ) throws ClassNotFoundException {
        for( LibraryLoader loader : loaders ) {
            Class<?> c = loader.getClass(cName);
            if (c != null) {
                return c;
            }
        }
        
        throw new ClassNotFoundException("Class is not in libraries.");
    }
    
    
    
    
    
    // -------------------------------------------------------------------------------------------------
    // ---------------------------  Data and Library Loading and Building  -----------------------------
    // -------------------------------------------------------------------------------------------------
    
    
    /**
     * Parses the provided file for data and adds it to the database.
     * @param file the file to be read in and parsed.
     * @throws IOException if the file does not exist or cannot be read.
     */
    public void addFileToNodes( File file ) throws IOException {
        logger.log("");
        logger.log("Processing file \"" + file.getName() + ".\"");
        
        // Read in the entire contents of the file at once, so that we can parse it easier
        Scanner baseScan = new Scanner(file);
        Scanner scan = baseScan.useDelimiter("\\Z");
        String content = scan.next();
        String[] lines = content.split("\n");
        baseScan.close();
        scan.close();
        
        // clean up the lines
        for (int index = 0; index < lines.length; index++) {
            lines[index] = cleanComments(lines[index], "//").trim();
        }
        
        boolean procNode = false;  // If we are currently processing a node
        int bracketNest = 0; // How far into the "nest" we are
        int firstIndex = 0; // The first index of current node being processed
        int lastIndex = 0; // The last index of current node being processed
        
        for (int index = 0; index < lines.length; index++) {
            
            // Get the next line
            String line = lines[index].trim();
            
            // If the line is an open bracket, then we are processing a node and have gone one deeper into the nest.
            if (line.equals("{")) {
                if (bracketNest == 0) firstIndex = index - 1; // The -1 is because the title line is one above us
                bracketNest++;
                procNode = true;
            }
            // If the line is a close bracket, then we are one node less deep into the nest.
            else if (line.equals("}")) {
                bracketNest--;
            }
            
            // If we are out of the nest completely and we are processing a node, then we are finished finding the node and
            //    should process it
            if (bracketNest == 0 && procNode) {
                procNode = false;
                lastIndex = index;
                processNode(Arrays.copyOfRange(lines, firstIndex, lastIndex + 1));
            }
        }
    }
    
    
    /**
     * A method that adds all jar files in the specified directory to the program as libraries.  It uses recursion to parse all subdirectories as well.
     * @param dirPath The path to the directory containing the jar files that are desired to be loaded.
     */
    public void addLoadersInDir( String dirPath ) {
        logger.log("");
        logger.incIndent();
        logger.log("Loading all jar files from directory \"" + dirPath + ".\"");
        
        // Create a new file that assumes the provided path is relative to the home directory of the application
        File folder = new File(Settings.defaultDirectory() + dirPath);
        
        // If the folder doesn't exist or isn't a directory, then it is not a path relative to the home folder of the application.
        //   In that case, assume that it is an absolute path.
        if (!folder.exists() || !folder.isDirectory()) {
            logger.log("Directory path \"" + dirPath + "\" was not a path relative to the base directory, trying as an absolute path.");
            folder = new File(dirPath);
        }
        
        // If the folder still doesn't exist or isn't a directory (either as a relative path or absolute path), then the folder is invalid
        //   and must be skipped.
        if (!folder.exists() || !folder.isDirectory()) {
            logger.log("Direcotry path \"" + dirPath + "\" could not be resolved as an absolute path to a directory, skipping.");
            return;
        }
        
        // Now that we have a valid directory, we need a ist of all of the files in the directory
        File[] fileList = folder.listFiles();
        
        // Loop through all files in the folder
        for(File curFile : fileList) {
            // If the current file is a directory, then use recursion to process the files within it as well
            if (curFile.isDirectory()) {
                logger.log("");
                logger.log("Found additional directory to parse for libraries: \"" + curFile.getAbsolutePath() + "\"");
                addLoadersInDir(curFile.getAbsolutePath());
            }
            
            // If the current file is not a directory, process it if it is a .jar file
            else {
                // Get the file extension
                int indexOfLastPeriod = curFile.getName().lastIndexOf(".");
                String ext = curFile.getName().substring(indexOfLastPeriod + 1);
                
                // If the file is a jar file, create a new LibraryLoader to load it
                if (ext.equals("jar")) {
                    logger.log("");
                    logger.log("Found jar file to add to libraries: \"" + curFile.getAbsolutePath() + "\"");
                    try {
                        LibraryLoader loader = new LibraryLoader(curFile.getAbsolutePath());
                        loader.loadClasses();
                        loaders.add(loader);
                    } 
                    // Exception handling - may be possible to make more graceful
                    catch (IOException ex) {
                        logger.log(ex);
                    }
                }
            }
        }
        logger.log("Completed loading libraries and plugins from directory \"" + dirPath + ".\"");
        logger.decIndent();
    }
    
    
    /**
     * A method that imports all .dat files from the specified directory into the database 
     * @param dirPath The path to the directory containing the dat files that are desired to be loaded.
     */
    public void addDataFromDir( String dirPath ) {
        logger.log("");
        logger.incIndent();
        logger.log("Loading all dat files from directory \"" + dirPath + ".\"");
        
        // Create a new file that assumes the provided path is relative to the home directory of the application
        File folder = new File(Settings.defaultDirectory() + dirPath);
        
        // If the folder doesn't exist or isn't a directory, then it is not a path relative to the home folder of the application.
        //   In that case, assume that it is an absolute path.
        if (!folder.exists() || !folder.isDirectory()) {
            logger.log("Directory path \"" + dirPath + "\" was not a path relative to the base directory, trying as an absolute path.");
            folder = new File(dirPath);
        }
        
        // If the folder still doesn't exist or isn't a directory (either as a relative path or absolute path), then the folder is invalid
        //   and must be skipped.
        if (!folder.exists() || !folder.isDirectory()) {
            logger.log("Direcotry path \"" + dirPath + "\" could not be resolved as an absolute path to a directory, skipping.");
            return;
        }
        
        // Now that we have a valid directory, we need a ist of all of the files in the directory
        File[] fileList = folder.listFiles();
        
        // Loop through all files in the folder
        for(File curFile : fileList) {
            
            // If the current file is a directory, then use recursion to process the files within it as well
            if (curFile.isDirectory()) {
                logger.log("");
                logger.log("Found additional directory to parse for dat files: \"" + curFile.getAbsolutePath() + "\"");
                addDataFromDir(curFile.getAbsolutePath());
            }
            
            // If the current file is not a directory, process it if it is a .dat file
            else {
                // Get the extension of the file
                int indexOfLastPeriod = curFile.getName().lastIndexOf(".");
                String ext = curFile.getName().substring(indexOfLastPeriod + 1);
                
                // If it is a data file, add it to the database
                if (ext.equals("dat")) {
                    logger.log("");
                    logger.log("Found dat file to add to the database: \"" + curFile.getAbsolutePath() + "\"");
                    try {
                        this.addFileToNodes(curFile);
                    } 
                    // Exception handling - may be possible to make more graceful
                    catch (IOException ex) {
                        logger.log(ex);
                    }
                }
            }
        }
        logger.log("Completed loading data from directory \"" + dirPath + ".\"");
        logger.decIndent();
    }
    
    
    /**
     * A method that builds the DataNodes into objects in the database.
     */
    public void buildObjects() {
        
        logger.log("");
        logger.log("Started building of objects.");
        
        int refCount = 0; // The number of unresolved references to objects still left
        
        int oldCount; // Stores the number of unresolved references to objects left after the last pass.
        
        do {
            
            oldCount = refCount;
            refCount = 0; // reset the count
            
            for (DataNode node : nodes) {
                // If the node has unresolved references, skip it and increase the count of unresolved references by 1
                if (node.hasUnresolvedDBRefs(this)) {
                    refCount++;
                }
                // If the node isn't already in the database, or been tried and found to be bad, add it to the database
                else if (!dataMap.containsKey(node.getName()) && !badObjects.contains(node.getName())) {
                    
                    Object o = node.toObject();
                    
                    if (o != null) {
                        logger.log("To Object: " + o);
                    }
                    else {
                        logger.log("To Object: null");
                    }
                    
                    if (o != null) {
                        dataMap.put(node.getName(), o);
                    }
                    else {
                        badObjects.add(node.getName());
                        logger.log("Creation of object \"" + node.getName() + "\" has failed, it is being added to the list of bad nodes.");
                    }
                }
            }
            
        } 
        // So long as we are still processing at least one node each round, continue looping.
        while (oldCount > refCount);
        
        logger.log("Completed building of objects.");

    }
    
    
    
    
    
    // -------------------------------------------------------------------------------------------------
    // ---------------------------  Internal Data Processing Methods  ----------------------------------
    // -------------------------------------------------------------------------------------------------
    
    
    /**
     * Processes a node represented by an array of lines and sends it to a specialized processing routine.
     * @param lines the lines representing the node
     */
    private void processNode( String[] lines ) {
        switch(lines[0]) {
            case "PATH":
                processPathNode(Arrays.copyOfRange(lines, 2, lines.length - 1));
                break;
            case "DATA":
                processDataNode(Arrays.copyOfRange(lines, 2, lines.length - 1));
                break;
            case "LIBRARY":
                processLibraryNode(Arrays.copyOfRange(lines, 2, lines.length - 1));
                break;
        }
    }
    
    
    /**
     * A method that processes a PATH node and moves the paths to the path map.
     * @param lines the lines making up the PATH node to be processed
     */
    private void processPathNode( String[] lines ) {
        logger.log("");
        logger.log("------------------------------------------------------------------------------------");
        logger.log("Started processing PATH node.");
        logger.log("------------------------------------------------------------------------------------");
        
        // Go through each line, and find a PATH mapping if there is one.
        for(String line : lines) {
            if(line.contains("=")) {
                int index = line.indexOf("=");
                
                String ref = line.substring(0, index).trim();
                String path = line.substring(index + 1).trim();
                
                pathMap.put(ref, path);
                logger.log("Putting \"" + ref + "\" to refer to \"" + path + ".\"");
            }
        }
        
        logger.log("Completed processing PATH node.");
    }
    
    
    /**
     * A method that processes a DATA node and creates DataNode objects for it.
     * @param lines the lines representing the DATA node
     */
    private void processDataNode( String[] lines ) {
        logger.log("");
        logger.log("------------------------------------------------------------------------------------");
        logger.log("Starting processing of DATA node.");
        logger.log("------------------------------------------------------------------------------------");
        
        int bracketNest = 0; // How far we are inside the nest
        int index = 0; // The current index of the line we are dealing with
        int indexOfStart = 0; // The starting index of the current node
        int indexOfFinish = 0; // The finish index of the current node
        boolean procNode = false; // Whether or not we are processing a node
        
        for( String line : lines ) {
            
            // Skip blank lines
            if (line.equals("")){
                index++; // But don't forget to increment the index!
                continue;
            }
            
            // If we are not processing a node and see a '|', then we have found a title node
            if (line.contains("|") && !procNode) {
                procNode = true;
                indexOfStart = index;
            }
            // If we see an open bracket, then we are one further into the nest
            else if (line.equals("{")) {
                bracketNest++;
            }
            // If we see a close bracket, then we are moving one out of the nest
            else if (line.equals("}")) {
                bracketNest--;
            }
            
            // If we are outside of the nest and it isn't the first index of the node,
            //    then we should create a new DataNode for this node.
            
            // Needs to add ArrayNode support
            if (bracketNest == 0 && (index != indexOfStart)) {
                procNode = false;
                indexOfFinish = index;
                
                logger.log("Creating new DataNode with title line \"" + lines[indexOfStart] + "\".");
                
                DataNode node = new DataNode(Arrays.copyOfRange(lines, indexOfStart, indexOfFinish + 1));
                nodes.add(node);
            }
            
            index++;
        }
        
        logger.log("Completed processing of DATA node.");
    }
    
    
    /**
     * A method that processes a LIBRARY node and creates LibraryLoader objects for the libraries.
     * @param lines the lines representing the LIBRARY node
     */
    private void processLibraryNode( String[] lines ) {
        
        logger.log("");
        logger.log("------------------------------------------------------------------------------------");
        logger.log("Starting processing LIBRARY node.");
        logger.log("------------------------------------------------------------------------------------");
        
        // Loop through the lines
        for (String line : lines) {
            
            // If the line contains a directory of libraries to load, load them all
            if (line.contains("DIRECTORY") && line.contains("=")) {
                int indexOfEquals = line.indexOf("=");
                String dirPath = line.substring(indexOfEquals + 1).trim();
                addLoadersInDir(dirPath);
            }
            
            // If the line points only to a specific jar file, load it
            else if (line.contains("JARFILE") && line.contains("=")) {
                int indexOfEquals = line.indexOf("=");
                String filePath = line.substring(indexOfEquals + 1).trim();
                
                LibraryLoader loader = null;
                
                try {
                    // First, assume that the filepath is relative to the base directory
                    try {
                        loader = new LibraryLoader(Settings.defaultDirectory() + filePath);
                    }
                    // If not, assume it is an absolute filepath
                    catch(IOException ex) {
                        logger.log("Failed to load \"" + filePath + "\" as a relative filepath, loading as absolute path.");
                        loader = new LibraryLoader(filePath);
                    }
                    loader.loadClasses();
                    loaders.add(loader);
                }
                catch(IOException ex) {
                    logger.log(ex);
                }
            }
        }
        
        logger.log("Completed processing LIBRARY node.");
    }
    
    
    
    
    
    // -------------------------------------------------------------------------------------------------
    // ---------------------------  State Variables  ---------------------------------------------------
    // -------------------------------------------------------------------------------------------------
    
    
    private Logger logger; // The logger to record what is gonig on
    private final TreeMap<String, Object> dataMap; // Stores the data in the database
    private final Collection<DataNode> nodes; // Stores the nodes that were read in from files
    private final TreeMap<String, String> pathMap; // Stores the PATH mappings for class paths
    private final Collection<LibraryLoader> loaders; // Stores the LibraryLoaders that contain the library classes
    private final Collection<String> badObjects; // The list of object names that have been discovered to be corrupt
    
    
    
    
    
    // -------------------------------------------------------------------------------------------------
    // ---------------------------  Static Methods and Variables  --------------------------------------
    // -------------------------------------------------------------------------------------------------
    
    
    private static final ObjectDB globalDB = new ObjectDB(); // The default global database
    
    /**
     * A method for getting the global database.
     * @return Returns the global ObjectDB.
     */
    public static ObjectDB getGlobalDB() {
        return globalDB;
    }
    
    
    /**
     * A method that cleans comments out of a provided string, with the comment marker provided.
     * @param line The String to be cleared of comments
     * @param comment The String indicating the start of a comment in the line
     * @return Returns the String provided that has been cleared of comments.
     */
    public static String cleanComments(String line, String comment) {
        
        // If the String does not contain the comment indicating string, then nothing
        //   needs to be done to it, and it can be returned as-is.
        if (!line.contains(comment)) {
            return line;
        }
        
        // Otherwise, return every part of the String that came before the comment started.
        int commentIndex = line.indexOf(comment);
        return line.substring(0, commentIndex);
    }
    
}
